package cmich.cps.svn.hooks;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Logger;
/**
 * Handles settings and pre-commit hook.
 * 
 * @author Jonathan Kissinger <kissi1j@cmich.edu>
 */
public class Settings {
	private String svnLook;
	private String javac;
	private Boolean requireCommitLog = null;
	private Boolean allowLateCommits = null;
	private Date dueDate = null;
	private String repoDir;
	private String reference;
	private String author;
	private List<String> directories = new ArrayList<String>();
	private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd 'at' hh:mm:ss aaaa");
	private String commit;
	private Thread thread;
	private boolean late;
	private String repoName; 
	public final String separator = ":";
	private final static Logger LOGGER = Logger.getLogger(Settings.class.getName());

	public Settings(String commit, String repoDir, String reference) {
		this.svnLook = "svnlook";
		this.javac = "javac";
		this.commit = commit;
		this.repoDir = repoDir.trim();
		LOGGER.config("Repository set to: " + this.repoDir);
		this.repoName = this.repoDir.split("/")[this.repoDir.split("/").length-1];
		LOGGER.config("Repository name set to: " + this.repoName);
		if(this.commit.equalsIgnoreCase("pre")) {
			this.reference = " -t " + reference + " ";
		}
		else if(this.commit.equalsIgnoreCase("post")) {
			this.reference = " -r " + reference + " ";
		}
		LOGGER.config("Reference set to: " + this.reference);
		this.author = execCommand(this.svnLook + " author" + this.reference + this.repoDir);
		if(this.author.endsWith("@CENTRAL.CMICH.LOCAL")) {
			this.author = this.author.split("@")[0];
		}
		LOGGER.config("Author set to: " + this.author);
	}

	/**
	 * Checks if {@link Settings#author} is a member of the professors group in
	 * the repository's authz file.
	 * 
	 * @return True if the author is a professor, false otherwise.
	 */
	public boolean isProf() {
		String authFile = this.repoDir + "/conf/authz";
		String professors = "";

		Properties prop = new Properties();
		try {
			prop.load(new FileInputStream(authFile));
			professors = prop.getProperty("professors");
		} catch (IOException e) {
			LOGGER.warning(e.toString());
		}

		LOGGER.fine("Professors: " + professors);
		String[] profArray = professors.split(",");
		for(int i=0;i<profArray.length;i++) {
			String professor = profArray[i];
			if(professor.startsWith("&")) {
				professor = professor.substring(1);
			}
			if(professor.equalsIgnoreCase(this.author)) {
				LOGGER.fine(this.author + " is a professor.");
				return true;
			}

		}
		return false;
	}

	/**
	 * Handles the situation where a student may commit to more than one assignment
	 * directory at a time by adding each directory to the {@link Settings#directories} List.
	 */
	public void setDirectories() {
		String[] dirs = execCommand(this.svnLook + " dirs-changed" + this.reference + this.repoDir).split("\n");
		String directory = "";
		LOGGER.finest("Checking if " + dirs.length + " directories fits the criteria.");
		for(int i=0;i<dirs.length;i++) {
			String[] paths = dirs[i].split("/");
			if(paths.length>2) {
				directory = "/" + paths[0] + "/" + paths[1];
				if(!this.directories.contains(directory)) {
					this.directories.add(directory);
					LOGGER.fine("Added directory: " + directory);
				}
			}
		}
		if(this.directories.size()==0) {
			LOGGER.info("No assignment level directories were changed, exiting normally.");
		}
	}

	/**
	 * Sends a given command to the runtime.  Any standard output or error output is written to a string
	 * which is returned upon termination of the child process or when it times out via InterruptTimerTask.
	 * 
	 * @param command String representation of the command to be sent to the runtime.
	 * @return String representing the results of the child process.
	 */
	public String execCommand(String command) {
		LOGGER.finest("Input: " + command);
		String output = "";
		boolean timedOut = false;
		int seconds = 300; //Timeout in seconds
		int timeout = seconds * 1000;
		try {
			final long startTime = System.nanoTime();
			Process child = Runtime.getRuntime().exec(command);
			Runnable readOut = new ReadOutput(child);
			Thread readerThread = new Thread(readOut);
			Timer timer = new Timer(true);
			try {
				readerThread.start();
				InterruptTimerTask interrupter = new InterruptTimerTask(Thread.currentThread());
				timer.schedule(interrupter, timeout);
				child.waitFor();
			} catch (InterruptedException e) {
				child.destroy();
				timedOut = true;
			} finally {
				LOGGER.finest("Command: " + command + " took " + (System.nanoTime()-startTime)/1000000000.0 + " seconds to execute.");
				timer.cancel();
				Thread.interrupted();
				readerThread.join();
				output = ((ReadOutput) readOut).getOutput();
			}
		} catch (IOException e) {
			if(e.toString().startsWith("java.io.IOException: Cannot run program") && e.toString().contains("svnadmin")) {
				LOGGER.fine("Expected exception: " + e.toString());
			}
			else {
				LOGGER.warning(e.toString());
			}
			output = e.toString();
		} catch (InterruptedException e) {
			LOGGER.warning(e.toString());
		}
		while(output.endsWith("\n") || output.endsWith("\r")) {
			output = output.substring(0, output.length()-1);
		}
		LOGGER.finest("Output: " + output);
		if(timedOut) {
			output = "Command timed out. Timeout=" + seconds;	
		}
		return output;
	}

	/**
	 * Checks the status of the precommit hooks.  If they are violated it exits with error code 1
	 * and a message for the student.
	 */
	public void preCommit(String dir) {
		LOGGER.fine("Called with dir: " + dir);
		if(this.requireCommitLog) {
			String comments = execCommand(this.svnLook + " log" + this.reference + this.repoDir);
			if(comments.equals("")) {
				System.err.println("There is no log attached to this commit.  Your professor has specified for this assignment that every commit must have a log message.");
				System.exit(1);
			}
			else {
				LOGGER.fine(comments);
			}
		}
		if(!this.allowLateCommits) {
			if(this.late) {
				System.err.println("This commit is past the due date: " + sdf.format(dueDate) + " and your professor has disallowed late commits.");
				System.exit(1);
			}
			else {
				LOGGER.fine("Today: " + sdf.format(Calendar.getInstance().getTime()) + " Due: " + sdf.format(dueDate));
			}
		}
	}

	/**
	 * Reads the properties of the current assignment directory and stores them in variables.
	 * 
	 * @param dir The current assignment directory.
	 */
	public void setProperties(String dir) {
		String commitLog = execCommand(this.svnLook + " propget " + this.repoDir + " requireCommitLog " + dir);
		String allowLate = execCommand(this.svnLook + " propget " + this.repoDir + " allowLateCommits " + dir);
		String dueDateStr = execCommand(this.svnLook + " propget " + this.repoDir + " dueDate " + dir);
		if(propertyIsNull(commitLog) || propertyIsNull(allowLate) || propertyIsNull(dueDateStr)) {
			if(this.commit.equals("pre")) {
				LOGGER.info("Properties not set on directory: " + dir + " Assuming professor doesn't want precommit hooks and exiting.");
				System.exit(0);
			}
			else {
				return;
			}
		}
		this.requireCommitLog = Boolean.parseBoolean(commitLog);
		this.allowLateCommits = Boolean.parseBoolean(allowLate);
		try {
			this.dueDate = this.sdf.parse(dueDateStr);
		} catch (ParseException e) {
			LOGGER.warning(e.toString());
		}
		this.late = this.dueDate.before(Calendar.getInstance().getTime());
	}

	/**
	 * @return A <code>File</code> object pointing to this jar file.
	 */
	public File getJarFile() {
		String path = Settings.class.getProtectionDomain().getCodeSource().getLocation().getPath();
		try {
			String decodedPath = URLDecoder.decode(path, "UTF-8");
			File myJar = new File(decodedPath);
			LOGGER.finest("This jar file: " + myJar);
			return myJar;
		} catch (UnsupportedEncodingException e) {
			LOGGER.warning(e.toString());
		}
		return null;
	}
	
	/**
	 * Checks if the passed property is not set on the assignment directory.
	 * 
	 * @param property
	 * @return
	 */
	public boolean propertyIsNull(String property) {
		return (property.contains("not found on path") || property.contains("does not exist"));
	}

	/**
	 * The {@link Settings#execCommand(String)} method puts this thread to sleep to wait for the
	 * child process to terminate.  This method allows that thread to be interrupted after a 
	 * period of time defined above.  Thus "timing out" the process.
	 */
	public void timedOut() {
		this.thread.interrupt();
	}

	/**
	 * Required for encapsulation of the {@link Settings#directories} List.
	 * 
	 * @param i The index of the directory to retrieve.
	 * @return the specified directory.
	 */
	public String getDirectory(int i) {
		return this.directories.get(i);
	}

	/**
	 * @return dueDate
	 */
	public Date getDueDate() {
		return this.dueDate;
	}

	/**
	 * Required for encapsulation of the {@link Settings#directories} List.
	 * 
	 * @return int representing the size of the directories List.
	 */
	public int numDirectories() {
		return this.directories.size();
	}	

	/**
	 * @return the boolean late
	 */
	public boolean isLate() {
		return this.late;
	}

	/**
	 * @return The author
	 */
	public String getAuthor() {
		return this.author;
	}

	/**
	 * @return svnLook command
	 */
	public String getSvnLook() {
		return this.svnLook;
	}

	/**
	 * @return Location of the repository
	 */
	public String getRepoDir() {
		return this.repoDir;
	}

	/**
	 * @return javac command
	 */
	public String getJavac() {
		return this.javac;
	}

	/**
	 * @return The name of the repository
	 */
	public String getRepoName() {
		return this.repoName;
	}
}

/**
 * Simple {@link TimerTask} that interrupts the given waiting thread when the timer completes.
 * 
 * @author Jonathan Kissinger <kissi1j@cmich.edu>
 */
class InterruptTimerTask extends TimerTask {
	private Thread thread;
	public InterruptTimerTask(Thread t) {
		this.thread = t;
	}
	public void run() {
		thread.interrupt();
	}
}

/**
 * Reads the output of a child process.
 * 
 * @author Jonathan Kissinger <kissi1j@cmich.edu>
 */
class ReadOutput implements Runnable {
	private String output = "";
	private Process child;
	public ReadOutput(Process child) {
		this.child = child;
	}
	@Override
	public void run() {
		try {
			BufferedReader in = new BufferedReader(new InputStreamReader(child.getInputStream()));
			String line = null;
			while ((line = in.readLine()) != null) {
				output = output + line + "\n";
			}
			in.close();

			BufferedReader error = new BufferedReader(new InputStreamReader(child.getErrorStream()));
			String errLine = null;
			while ((errLine = error.readLine()) != null) {
				output = output + errLine + "\n";
			}
			error.close();
		} catch(IOException e) {
			e.printStackTrace();
		}
	}
	public String getOutput() {
		return this.output;
	}
}